import { BoundingSphere } from '../../Source/Cesium.js';
import { BoxGeometry } from '../../Source/Cesium.js';
import { Cartesian3 } from '../../Source/Cesium.js';
import { ColorGeometryInstanceAttribute } from '../../Source/Cesium.js';
import { ComponentDatatype } from '../../Source/Cesium.js';
import { CylinderGeometry } from '../../Source/Cesium.js';
import { defined } from '../../Source/Cesium.js';
import { DistanceDisplayConditionGeometryInstanceAttribute } from '../../Source/Cesium.js';
import { Ellipsoid } from '../../Source/Cesium.js';
import { Geometry } from '../../Source/Cesium.js';
import { GeometryAttribute } from '../../Source/Cesium.js';
import { GeometryInstance } from '../../Source/Cesium.js';
import { GeometryInstanceAttribute } from '../../Source/Cesium.js';
import { HeadingPitchRange } from '../../Source/Cesium.js';
import { Math as CesiumMath } from '../../Source/Cesium.js';
import { Matrix4 } from '../../Source/Cesium.js';
import { PerspectiveFrustum } from '../../Source/Cesium.js';
import { PolygonGeometry } from '../../Source/Cesium.js';
import { PrimitiveType } from '../../Source/Cesium.js';
import { Rectangle } from '../../Source/Cesium.js';
import { RectangleGeometry } from '../../Source/Cesium.js';
import { ShowGeometryInstanceAttribute } from '../../Source/Cesium.js';
import { Transforms } from '../../Source/Cesium.js';
import { Camera } from '../../Source/Cesium.js';
import { MaterialAppearance } from '../../Source/Cesium.js';
import { PerInstanceColorAppearance } from '../../Source/Cesium.js';
import { Primitive } from '../../Source/Cesium.js';
import { SceneMode } from '../../Source/Cesium.js';
import BadGeometry from '../BadGeometry.js';
import createContext from '../createContext.js';
import createFrameState from '../createFrameState.js';
import createScene from '../createScene.js';
import pollToPromise from '../pollToPromise.js';

describe('Scene/Primitive', function() {

    var scene;
    var context;

    var frameStateContext;
    var frameState;

    var ellipsoid;

    var rectangle1;
    var rectangle2;

    var rectangleInstance1;
    var rectangleInstance2;

    var primitive;

    beforeAll(function() {
        scene = createScene();
        scene.primitives.destroyPrimitives = false;
        context = scene.context;
        ellipsoid = Ellipsoid.WGS84;

        frameStateContext = createContext();
    });

    afterAll(function() {
        scene.destroyForSpecs();
        frameStateContext.destroyForSpecs();
    });

    beforeEach(function() {
        scene.morphTo3D(0);

        var camera = scene.camera;
        camera.frustum = new PerspectiveFrustum();
        camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
        camera.frustum.fov = CesiumMath.toRadians(60.0);

        scene.frameState.passes.render = true;
        scene.frameState.passes.pick = false;

        // Mock frameState, separate from scene.frameState, used for test that call primitive.update directly
        frameState = createFrameState(frameStateContext);

        rectangle1 = Rectangle.fromDegrees(-80.0, 20.0, -70.0, 30.0);
        rectangle2 = Rectangle.fromDegrees(70.0, 20.0, 80.0, 30.0);

        var translation = Cartesian3.multiplyByScalar(Cartesian3.normalize(ellipsoid.cartographicToCartesian(Rectangle.center(rectangle1)), new Cartesian3()), 2.0, new Cartesian3());
        rectangleInstance1 = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rectangle1
            }),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            id : 'rectangle1',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
                show : new ShowGeometryInstanceAttribute(true)
            }
        });

        translation = Cartesian3.multiplyByScalar(Cartesian3.normalize(ellipsoid.cartographicToCartesian(Rectangle.center(rectangle2)), new Cartesian3()), 3.0, new Cartesian3());
        rectangleInstance2 = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rectangle2
            }),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            id : 'rectangle2',
            attributes : {
                color : new ColorGeometryInstanceAttribute(0.0, 1.0, 1.0, 1.0),
                show : new ShowGeometryInstanceAttribute(true)
            }
        });
    });

    afterEach(function() {
        scene.primitives.removeAll();
        primitive = primitive && !primitive.isDestroyed() && primitive.destroy();
    });

    it('default constructs', function() {
        primitive = new Primitive();
        expect(primitive.geometryInstances).not.toBeDefined();
        expect(primitive.appearance).not.toBeDefined();
        expect(primitive.depthFailAppearance).not.toBeDefined();
        expect(primitive.modelMatrix).toEqual(Matrix4.IDENTITY);
        expect(primitive.show).toEqual(true);
        expect(primitive.vertexCacheOptimize).toEqual(false);
        expect(primitive.interleave).toEqual(false);
        expect(primitive.compressVertices).toEqual(true);
        expect(primitive.releaseGeometryInstances).toEqual(true);
        expect(primitive.allowPicking).toEqual(true);
        expect(primitive.cull).toEqual(true);
        expect(primitive.asynchronous).toEqual(true);
        expect(primitive.debugShowBoundingVolume).toEqual(false);
    });

    it('Constructs with options', function() {
        var geometryInstances = {};
        var appearance = {};
        var depthFailAppearance = {};
        var modelMatrix = Matrix4.fromUniformScale(5.0);

        primitive = new Primitive({
            geometryInstances : geometryInstances,
            appearance : appearance,
            depthFailAppearance : depthFailAppearance,
            modelMatrix : modelMatrix,
            show : false,
            vertexCacheOptimize : true,
            interleave : true,
            compressVertices : false,
            releaseGeometryInstances : false,
            allowPicking : false,
            cull : false,
            asynchronous : false,
            debugShowBoundingVolume : true
        });

        expect(primitive.geometryInstances).toEqual(geometryInstances);
        expect(primitive.appearance).toEqual(appearance);
        expect(primitive.depthFailAppearance).toEqual(depthFailAppearance);
        expect(primitive.modelMatrix).toEqual(modelMatrix);
        expect(primitive.show).toEqual(false);
        expect(primitive.vertexCacheOptimize).toEqual(true);
        expect(primitive.interleave).toEqual(true);
        expect(primitive.compressVertices).toEqual(false);
        expect(primitive.releaseGeometryInstances).toEqual(false);
        expect(primitive.allowPicking).toEqual(false);
        expect(primitive.cull).toEqual(false);
        expect(primitive.asynchronous).toEqual(false);
        expect(primitive.debugShowBoundingVolume).toEqual(true);
    });

    it('releases geometry instances when releaseGeometryInstances is true', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            releaseGeometryInstances : true,
            asynchronous : false
        });

        expect(primitive.geometryInstances).toBeDefined();
        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(primitive.geometryInstances).not.toBeDefined();
    });

    it('does not release geometry instances when releaseGeometryInstances is false', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            releaseGeometryInstances : false,
            asynchronous : false
        });

        expect(primitive.geometryInstances).toBeDefined();
        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(primitive.geometryInstances).toBeDefined();
    });

    it('adds afterRender promise to frame state', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            releaseGeometryInstances : false,
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();

        return primitive.readyPromise.then(function(param) {
            expect(param.ready).toBe(true);
        });
    });

    it('does not render when geometryInstances is an empty array', function() {
        primitive = new Primitive({
            geometryInstances : [],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.commandList.length = 0;
        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    it('does not render when show is false', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        primitive.update(frameState);
        expect(frameState.commandList.length).toBeGreaterThan(0);

        frameState.commandList.length = 0;
        primitive.show = false;
        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    it('does not render other than for the color or pick pass', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.passes.render = false;
        frameState.passes.pick = false;

        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    it('does not render when scene3DOnly is true and the scene mode is SCENE2D', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.mode = SceneMode.SCENE2D;
        frameState.scene3DOnly = true;

        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    it('does not render when scene3DOnly is true and the scene mode is COLUMBUS_VIEW', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.mode = SceneMode.COLUMBUS_VIEW;
        frameState.scene3DOnly = true;

        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    it('renders in two passes for closed, translucent geometry', function() {
        primitive = new Primitive({
            geometryInstances : new GeometryInstance({
                geometry : BoxGeometry.fromDimensions({
                    vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                    dimensions : new Cartesian3(500000.0, 500000.0, 500000.0)
                }),
                id : 'box',
                attributes : {
                    color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 0.5)
                }
            }),
            appearance : new PerInstanceColorAppearance({
                closed : true,
                translucent : true
            }),
            asynchronous : false
        });

        var frameState = scene.frameState;
        frameState.commandList.length = 0;

        // set scene3DOnly to true so that the geometry is not split due to the IDL
        frameState.scene3DOnly = true;
        scene.primitives.add(primitive);
        scene.render();
        expect(frameState.commandList.length).toEqual(2);
    });

    function verifyPrimitiveRender(primitive, rectangle) {
        scene.primitives.removeAll();
        if (defined(rectangle)){
            scene.camera.setView({ destination : rectangle });
        }
        expect(scene).toRender([0, 0, 0, 255]);

        scene.primitives.add(primitive);
        expect(scene).notToRender([0, 0, 0, 255]);
    }

    it('renders in Columbus view when scene3DOnly is false', function() {
        scene.frameState.scene3DOnly = false;
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.morphToColumbusView(0);
        verifyPrimitiveRender(primitive, rectangle1);
        verifyPrimitiveRender(primitive, rectangle2);
    });

    it('renders in 2D when scene3DOnly is false', function() {
        scene.frameState.scene3DOnly = false;
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.morphTo2D(0);
        verifyPrimitiveRender(primitive, rectangle1);
        verifyPrimitiveRender(primitive, rectangle2);
    });

    it('renders RTC', function() {
        var dimensions = new Cartesian3(400.0, 300.0, 500.0);
        var positionOnEllipsoid = Cartesian3.fromDegrees(-105.0, 45.0);
        var boxModelMatrix = Matrix4.multiplyByTranslation(
                Transforms.eastNorthUpToFixedFrame(positionOnEllipsoid),
                new Cartesian3(0.0, 0.0, dimensions.z * 0.5), new Matrix4());

        var boxGeometry = BoxGeometry.createGeometry(BoxGeometry.fromDimensions({
            vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
            dimensions : dimensions
        }));

        var positions = boxGeometry.attributes.position.values;
        var newPositions = new Float32Array(positions.length);
        for (var i = 0; i < positions.length; ++i) {
            newPositions[i] = positions[i];
        }
        boxGeometry.attributes.position.values = newPositions;
        boxGeometry.attributes.position.componentDatatype = ComponentDatatype.FLOAT;

        BoundingSphere.transform(boxGeometry.boundingSphere, boxModelMatrix, boxGeometry.boundingSphere);

        var boxGeometryInstance = new GeometryInstance({
            geometry : boxGeometry,
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 0.5)
            }
        });

        var primitive = new Primitive({
            geometryInstances : boxGeometryInstance,
            appearance : new PerInstanceColorAppearance({
                closed: true
            }),
            asynchronous : false,
            allowPicking : false,
            rtcCenter : boxGeometry.boundingSphere.center
        });

        // create test camera
        var camera = scene.camera;
        var testCamera = new Camera(scene);
        testCamera.viewBoundingSphere(boxGeometry.boundingSphere);
        scene.camera = testCamera;

        scene.frameState.scene3DOnly = true;
        verifyPrimitiveRender(primitive);

        scene.camera = camera;
    });

    it('renders with depth fail appearance', function() {
        var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
        var translation = Cartesian3.multiplyByScalar(Cartesian3.normalize(ellipsoid.cartographicToCartesian(Rectangle.center(rect)), new Cartesian3()), 100.0, new Cartesian3());
        var rectInstance = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rect
            }),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            id : 'rect',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0)
            }
        });
        var p0 = new Primitive({
            geometryInstances : rectInstance,
            appearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            asynchronous : false
        });

        var rectInstance2 = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rect
            }),
            id : 'rect2',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0),
                depthFailColor : new ColorGeometryInstanceAttribute(1.0, 0.0, 1.0, 1.0)
            }
        });
        var p1 = new Primitive({
            geometryInstances : rectInstance2,
            appearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            depthFailAppearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            asynchronous : false
        });

        scene.primitives.add(p0);
        scene.primitives.add(p1);
        scene.camera.setView({ destination : rect });
        scene.renderForSpecs();

        expect(scene).toRender([255, 0, 255, 255]);
    });

    it('pick with depth fail appearance', function() {
        var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
        var translation = Cartesian3.multiplyByScalar(Cartesian3.normalize(ellipsoid.cartographicToCartesian(Rectangle.center(rect)), new Cartesian3()), 100.0, new Cartesian3());
        var rectInstance = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rect
            }),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            id : 'rect',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0)
            }
        });
        var p0 = new Primitive({
            geometryInstances : rectInstance,
            appearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            asynchronous : false
        });

        var rectInstance2 = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rect
            }),
            id : 'rect2',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0),
                depthFailColor : new ColorGeometryInstanceAttribute(1.0, 0.0, 1.0, 1.0)
            }
        });
        var p1 = new Primitive({
            geometryInstances : rectInstance2,
            appearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            depthFailAppearance : new PerInstanceColorAppearance({
                translucent : false
            }),
            asynchronous : false
        });

        scene.primitives.add(p0);
        scene.primitives.add(p1);
        scene.camera.setView({ destination : rect });
        scene.renderForSpecs();

        expect(scene).toPickAndCall(function(result) {
            expect(result.primitive).toEqual(p1);
            expect(result.id).toEqual('rect2');
        });
    });

    it('RTC throws with more than one instance', function() {
        expect(function() {
            return new Primitive({
                geometryInstances : [rectangleInstance1, rectangleInstance2],
                appearance : new PerInstanceColorAppearance({
                    closed: true
                }),
                asynchronous : false,
                allowPicking : false,
                rtcCenter : Cartesian3.ZERO
            });
        }).toThrowDeveloperError();
    });

    it('RTC throws if the scene is not 3D only', function() {
        scene.frameState.scene3DOnly = false;
        var primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance({
                closed: true
            }),
            asynchronous : false,
            allowPicking : false,
            rtcCenter : Cartesian3.ZERO
        });

        expect(function() {
            verifyPrimitiveRender(primitive);
        }).toThrowDeveloperError();
    });

    it('updates model matrix for one instance in 3D', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        primitive.update(frameState);
        var commands = frameState.commandList;
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(primitive.modelMatrix);

        var modelMatrix = Matrix4.fromUniformScale(10.0);
        primitive.modelMatrix = modelMatrix;

        commands.length = 0;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(modelMatrix);
    });

    it('updates model matrix for more than one instance in 3D with equal model matrices in 3D only scene', function() {
        var modelMatrix = Matrix4.fromUniformScale(2.0);
        rectangleInstance1.modelMatrix = modelMatrix;
        rectangleInstance2.modelMatrix = modelMatrix;

        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.scene3DOnly = true;

        var commands = frameState.commandList;
        commands.length = 0;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(modelMatrix);

        modelMatrix = Matrix4.fromUniformScale(10.0);
        primitive.modelMatrix = modelMatrix;

        commands.length = 0;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(modelMatrix);
    });

    it('computes model matrix when given one for a single instance and for the primitive in 3D only', function() {
        var instanceModelMatrix = Matrix4.fromUniformScale(2.0);

        var dimensions = new Cartesian3(400000.0, 300000.0, 500000.0);
        var positionOnEllipsoid = Cartesian3.fromDegrees(-105.0, 45.0);
        var primitiveModelMatrix = Matrix4.multiplyByTranslation(
            Transforms.eastNorthUpToFixedFrame(positionOnEllipsoid),
            new Cartesian3(0.0, 0.0, dimensions.z * 0.5), new Matrix4());

        var boxGeometry = BoxGeometry.fromDimensions({
            vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
            dimensions : dimensions
        });
        var boxGeometryInstance = new GeometryInstance({
            geometry : boxGeometry,
            modelMatrix : instanceModelMatrix,
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0)
            }
        });
        primitive = new Primitive({
            geometryInstances : boxGeometryInstance,
            modelMatrix : primitiveModelMatrix,
            appearance : new PerInstanceColorAppearance({
                translucent : false,
                closed: true
            }),
            asynchronous : false
        });

        var expectedModelMatrix = Matrix4.multiplyTransformation(primitiveModelMatrix, instanceModelMatrix, new Matrix4());

        frameState.scene3DOnly = true;

        var commands = frameState.commandList;
        commands.length = 0;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(expectedModelMatrix);
    });

    it('update model matrix throws in Columbus view', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.mode = SceneMode.COLUMBUS_VIEW;
        frameState.scene3DOnly = false;

        var commands = frameState.commandList;
        commands.length = 0;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(Matrix4.IDENTITY);

        var modelMatrix = Matrix4.fromUniformScale(10.0);
        primitive.modelMatrix = modelMatrix;

        commands.length = 0;
        expect(function() {
            primitive.update(frameState);
        }).toThrowDeveloperError();
    });

    it('update model matrix throws in 2D', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        frameState.mode = SceneMode.SCENE2D;
        frameState.scene3DOnly = false;

        var commands = frameState.commandList;
        primitive.update(frameState);
        expect(commands.length).toEqual(1);
        expect(commands[0].modelMatrix).toEqual(Matrix4.IDENTITY);

        var modelMatrix = Matrix4.fromUniformScale(10.0);
        primitive.modelMatrix = modelMatrix;

        commands.length = 0;
        expect(function() {
            primitive.update(frameState);
        }).toThrowDeveloperError();
    });

    it('renders bounding volume with debugShowBoundingVolume', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false,
            debugShowBoundingVolume : true
        });

        scene.primitives.add(primitive);
        scene.camera.setView({ destination : rectangle1 });
        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba[0]).not.toEqual(0);
            expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[3]).toEqual(255);
        });
    });

    it('transforms to world coordinates', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        verifyPrimitiveRender(primitive, rectangle1);
        verifyPrimitiveRender(primitive, rectangle2);
        expect(primitive.modelMatrix).toEqual(Matrix4.IDENTITY);
    });

    it('does not transform to world coordinates', function() {
        rectangleInstance2.modelMatrix = Matrix4.clone(rectangleInstance1.modelMatrix);
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.frameState.scene3DOnly = true;
        verifyPrimitiveRender(primitive, rectangle1);
        verifyPrimitiveRender(primitive, rectangle2);
        expect(primitive.modelMatrix).not.toEqual(Matrix4.IDENTITY);
        scene.frameState.scene3DOnly = true;
    });

    it('get common per instance attributes', function() {
        rectangleInstance2.attributes.not_used = new GeometryInstanceAttribute({
            componentDatatype : ComponentDatatype.FLOAT,
            componentsPerAttribute : 1,
            value : [0.5]
        });

        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();

        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
        expect(attributes.color).toBeDefined();
        expect(attributes.show).toBeDefined();

        attributes = primitive.getGeometryInstanceAttributes('rectangle2');
        expect(attributes.color).toBeDefined();
        expect(attributes.show).toBeDefined();
        expect(attributes.not_used).not.toBeDefined();
    });

    it('modify color instance attribute', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.camera.setView({ destination : rectangle1 });
        scene.primitives.add(primitive);
        var pixels;
        expect(scene).toRenderAndCall(function(rgba) {
            pixels = rgba;
            expect(rgba).not.toEqual([0, 0, 0, 255]);
        });

        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
        expect(attributes.color).toBeDefined();
        attributes.color = [255, 255, 255, 255];

        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba).not.toEqual([0, 0, 0, 255]);
            expect(rgba).not.toEqual(pixels);
        });
    });

    it('modify show instance attribute', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.camera.setView({ destination : rectangle1 });
        expect(scene).notToRender([0, 0, 0, 255]);

        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
        expect(attributes.show).toBeDefined();
        attributes.show = [0];

        expect(scene).toRender([0, 0, 0, 255]);
    });

    it('get bounding sphere from per instance attribute', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        verifyPrimitiveRender(primitive, rectangle1);

        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
        expect(attributes.boundingSphere).toBeDefined();
    });

    it('renders with distance display condition per instance attribute', function() {
        if (!context.floatingPointTexture) {
            return;
        }

        var near = 10000.0;
        var far = 1000000.0;
        var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
        var translation = Cartesian3.multiplyByScalar(Cartesian3.normalize(ellipsoid.cartographicToCartesian(Rectangle.center(rect)), new Cartesian3()), 2.0, new Cartesian3());
        var rectInstance = new GeometryInstance({
            geometry : new RectangleGeometry({
                vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
                ellipsoid : ellipsoid,
                rectangle : rect
            }),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            id : 'rect',
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
                distanceDisplayCondition : new DistanceDisplayConditionGeometryInstanceAttribute(near, far)
            }
        });

        primitive = new Primitive({
            geometryInstances : rectInstance,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.camera.setView({ destination : rect });
        scene.renderForSpecs();

        var boundingSphere = primitive.getGeometryInstanceAttributes('rect').boundingSphere;
        var center = boundingSphere.center;
        var radius = boundingSphere.radius;

        scene.camera.lookAt(center, new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius));
        expect(scene).toRender([0, 0, 0, 255]);

        scene.camera.lookAt(center, new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + near + 1.0));
        expect(scene).notToRender([0, 0, 0, 255]);

        scene.camera.lookAt(center, new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + far + 1.0));
        expect(scene).toRender([0, 0, 0, 255]);
    });

    it('primitive with display condition properly transforms boundingSphere', function() {
        var near = 10000.0;
        var far = 1000000.0;
        var translation = new Cartesian3(10, 20, 30);

        var cylinder = new GeometryInstance({
            id : 'cylinder',
            vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
            geometry : new CylinderGeometry({
                length : 10,
                topRadius : 10,
                bottomRadius : 10
            }),
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
                show : new ShowGeometryInstanceAttribute(true),
                distanceDisplayCondition : new DistanceDisplayConditionGeometryInstanceAttribute(near, far)
            }
        });

        primitive = new Primitive({
            geometryInstances : cylinder,
            appearance : new PerInstanceColorAppearance(),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.frameState.scene3DOnly = true;
        scene.renderForSpecs();

        var boundingSphere = primitive.getGeometryInstanceAttributes('cylinder').boundingSphere;
        var center = boundingSphere.center;
        expect(center).toEqual(translation);
    });

    it('primitive without display condition properly transforms boundingSphere', function() {
        var translation = new Cartesian3(10, 20, 30);

        var cylinder = new GeometryInstance({
            id : 'cylinder',
            vertexFormat : PerInstanceColorAppearance.VERTEX_FORMAT,
            geometry : new CylinderGeometry({
                length : 10,
                topRadius : 10,
                bottomRadius : 10
            }),
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
                show : new ShowGeometryInstanceAttribute(true)
            }
        });

        primitive = new Primitive({
            geometryInstances : cylinder,
            appearance : new PerInstanceColorAppearance(),
            modelMatrix : Matrix4.fromTranslation(translation, new Matrix4()),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.frameState.scene3DOnly = true;
        scene.renderForSpecs();

        var boundingSphere = primitive.getGeometryInstanceAttributes('cylinder').boundingSphere;
        var center = boundingSphere.center;
        expect(center).toEqual(translation);
    });

    it('getGeometryInstanceAttributes returns same object each time', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        verifyPrimitiveRender(primitive, rectangle1);

        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
        var attributes2 = primitive.getGeometryInstanceAttributes('rectangle1');
        expect(attributes).toBe(attributes2);
    });

    it('picking', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        verifyPrimitiveRender(primitive, rectangle1);

        expect(scene).toPickAndCall(function(result) {
            expect(result.primitive).toEqual(primitive);
            expect(result.id).toEqual('rectangle1');
        });

        verifyPrimitiveRender(primitive, rectangle2);

        expect(scene).toPickAndCall(function(result) {
            expect(result.primitive).toEqual(primitive);
            expect(result.id).toEqual('rectangle2');
        });
    });

    it('does not pick when allowPicking is false', function() {
        primitive = new Primitive({
            geometryInstances : [rectangleInstance1],
            appearance : new PerInstanceColorAppearance(),
            allowPicking : false,
            asynchronous : false
        });

        verifyPrimitiveRender(primitive, rectangle1);

        expect(scene).notToPick();
    });

    it('does not cull when cull is false', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false,
            cull : false
        });

        frameState.commandList.length = 0;
        primitive.update(frameState);
        expect(frameState.commandList[0].cull).toEqual(false);
    });

    it('update throws when geometry primitive types are different', function() {
        primitive = new Primitive({
            geometryInstances : [
                new GeometryInstance({
                    geometry : new Geometry({
                        attributes : {
                            position : new GeometryAttribute({
                                componentDatatype : ComponentDatatype.FLOAT,
                                componentsPerAttribute : 3,
                                values : new Float32Array([1.0, 2.0, 3.0, 4.0])
                            })
                        },
                        primitiveType : PrimitiveType.LINES
                    })
                }),
                new GeometryInstance({
                    geometry : new Geometry({
                        attributes : {
                            position : new GeometryAttribute({
                                componentDatatype : ComponentDatatype.FLOAT,
                                componentsPerAttribute : 3,
                                values : new Float32Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
                            })
                        },
                        primitiveType : PrimitiveType.TRIANGLES
                    })
                })
            ],
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        expect(function() {
            primitive.update(frameState);
        }).toThrowDeveloperError();
    });

    it('failed geometry rejects promise and throws on next update', function() {
        primitive = new Primitive({
            geometryInstances : [new GeometryInstance({
                geometry : new BadGeometry()
            })],
            appearance : new MaterialAppearance({
                materialSupport : MaterialAppearance.MaterialSupport.ALL
            }),
            compressVertices : false
        });

        scene.frameState.afterRender.length = 0;
        scene.primitives.add(primitive);

        return pollToPromise(function() {
            for (var i = 0; i < frameState.afterRender.length; ++i) {
                frameState.afterRender[i]();
                return true;
            }

            primitive.update(frameState);
            return false;
        }).then(function() {
            return primitive.readyPromise.then(function() {
                fail('should not be called');
            }).otherwise(function(e) {
                expect(e).toBe(primitive._error);
                // Use toThrow since the error is thrown by RequireJS for the web worker import script
                expect(function() {
                    scene.render();
                }).toThrow();
            });
        });
    });

    it('internally invalid asynchronous geometry resolves promise and sets ready', function() {
        primitive = new Primitive({
            geometryInstances : [new GeometryInstance({
                geometry : PolygonGeometry.fromPositions({
                    positions : []
                })
            })],
            appearance : new MaterialAppearance({
                materialSupport : MaterialAppearance.MaterialSupport.ALL
            }),
            compressVertices : false
        });

        scene.frameState.afterRender.length = 0;

        return pollToPromise(function() {
            for (var i = 0; i < frameState.afterRender.length; ++i) {
                frameState.afterRender[i]();
                return true;
            }

            primitive.update(frameState);
            return false;
        }).then(function() {
            return primitive.readyPromise.then(function(arg) {
                expect(arg).toBe(primitive);
                expect(primitive.ready).toBe(true);
            });
        });
    });

    it('internally invalid synchronous geometry resolves promise and sets ready', function() {
        primitive = new Primitive({
            geometryInstances : [new GeometryInstance({
                geometry : PolygonGeometry.fromPositions({
                    positions : []
                })
            })],
            appearance : new MaterialAppearance({
                materialSupport : MaterialAppearance.MaterialSupport.ALL
            }),
            asynchronous : false,
            compressVertices : false
        });

        scene.frameState.afterRender.length = 0;

        return pollToPromise(function() {
            if (scene.frameState.afterRender.length > 0) {
                scene.frameState.afterRender[0]();
                return true;
            }
            primitive.update(scene.frameState);
            return false;
        }).then(function() {
            return primitive.readyPromise.then(function(arg) {
                expect(arg).toBe(primitive);
                expect(primitive.ready).toBe(true);
            });
        });
    });

    it('can mix valid and invalid geometry', function() {
        var instances = [];
        instances.push(rectangleInstance1);
        instances.push(new GeometryInstance({
            geometry : PolygonGeometry.fromPositions({
                positions : []
            }),
            attributes : {
                color : new ColorGeometryInstanceAttribute(1.0, 0.0, 1.0, 1.0)
            },
            id : 'invalid'
        }));
        instances.push(rectangleInstance2);

        primitive = new Primitive({
            geometryInstances : instances,
            appearance : new PerInstanceColorAppearance({
                flat : true
            })
        });

        return pollToPromise(function() {
            primitive.update(frameState);
            if (frameState.afterRender.length > 0) {
                frameState.afterRender[0]();
            }
            return primitive.ready;
        }).then(function() {
            expect(primitive.getGeometryInstanceAttributes('rectangle1').boundingSphere).toBeDefined();
            expect(primitive.getGeometryInstanceAttributes('rectangle2').boundingSphere).toBeDefined();
            expect(primitive.getGeometryInstanceAttributes('invalid').boundingSphere).not.toBeDefined();
        });
    });

    it('shader validation', function() {
        if (!!window.webglStub) {
            return;
        }

        primitive = new Primitive({
            geometryInstances : [rectangleInstance1, rectangleInstance2],
            appearance : new MaterialAppearance({
                materialSupport : MaterialAppearance.MaterialSupport.ALL
            }),
            asynchronous : false,
            compressVertices : false
        });

        expect(function() {
            primitive.update(frameState);
        }).toThrowDeveloperError();
    });

    it('setting per instance attribute throws when value is undefined', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        primitive.update(frameState);
        var attributes = primitive.getGeometryInstanceAttributes('rectangle1');

        expect(function() {
            attributes.color = undefined;
        }).toThrowDeveloperError();
    });

    it('can disable picking when asynchronous', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : true,
            allowPicking : false
        });

        frameState.afterRender.length = 0;
        scene.primitives.add(primitive);

        return pollToPromise(function() {
            if (frameState.afterRender.length > 0) {
                frameState.afterRender[0]();
            }
            scene.render();
            return primitive.ready;
        }).then(function() {
            var attributes = primitive.getGeometryInstanceAttributes('rectangle1');
            expect(function() {
                attributes.color = undefined;
            }).toThrowDeveloperError();
        });
    });

    it('getGeometryInstanceAttributes throws without id', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();

        expect(function() {
            primitive.getGeometryInstanceAttributes();
        }).toThrowDeveloperError();
    });

    it('getGeometryInstanceAttributes throws if update was not called', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        expect(function() {
            primitive.getGeometryInstanceAttributes('rectangle1');
        }).toThrowDeveloperError();
    });

    it('getGeometryInstanceAttributes returns undefined if id does not exist', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();

        expect(primitive.getGeometryInstanceAttributes('unknown')).not.toBeDefined();
    });

    it('isDestroyed', function() {
        primitive = new Primitive();
        expect(primitive.isDestroyed()).toEqual(false);
        primitive.destroy();
        expect(primitive.isDestroyed()).toEqual(true);
    });

    it('renders when using asynchronous pipeline', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance({
                flat : true
            })
        });

        var frameState = scene.frameState;
        return pollToPromise(function() {
            primitive.update(frameState);
            for (var i = 0; i < frameState.afterRender.length; ++i) {
                frameState.afterRender[i]();
            }
            return primitive.ready;
        }).then(function() {
            verifyPrimitiveRender(primitive, rectangle1);
        });
    });

    it('destroy before asynchronous pipeline is complete', function() {
        primitive = new Primitive({
            geometryInstances : rectangleInstance1,
            appearance : new PerInstanceColorAppearance()
        });

        primitive.update(frameState);

        primitive.destroy();
        expect(primitive.isDestroyed()).toEqual(true);
    });
}, 'WebGL');
